組込み現場の「C++」プログラミング 明日から使える徹底入門

高木 信尚(株式会社クローバーフィールド

4.6 Cと共通の注意点

ここでは,CとC++に共通した移植性に関する注意点を挙げることにします.ただし,OSやペリフェラルの違いに関する注意点はあえて紹介するまでもありませんので,言語レベルの内容に限定することにします.

4.6.1 バイトオーダーとビットオーダー

「バイトオーダー」(エンディアン,エンディアンネス)は,複数バイトで構成されるスカラ型(汎整数型,浮動小数点型,ポインタ型)が,どのようにメモリ上に配置されるかという概念です.

たとえば,1バイトが8ビットの処理系において16ビットの符号無し整数型を考えた場合,その値が16進数で0x1234のとき,以下の上のようになる場合と下のようになる場合があります.

アドレスのオフセット+0+1
格納される値(16進数)1234

アドレスのオフセット+0+1
格納される値(16進数)3412

前者の方式を「ビッグエンディアン」,後者の方式を「リトルエンディアン」と呼びます.ちなみに,ビッグエンディアンとかリトルエンディアンという呼び名は,スウィフトの『ガリバー旅行記』の小人国に登場する,卵の丸いほうから割る人々(ビッグエンディアン)と卵のとがったほうから割る人々(リトルエンディアン)に由来しています.

バイトオーダーはプロセッサのアーキテクチャによって異なるので,ターゲットが変わればバイトオーダーも変わる可能性があります.また,アーキテクチャが異なるプロセッサ間で通信を行う場合にも,バイトオーダーに注意する必要があります.

バイトオーダーに対して,「ビットオーダー」という概念もあります.C/C++では,ビットフィールドが上位ビットから割り付けられるのか,下位ビットから割り付けられるのかの違いということになります.バイトオーダーとは異なり,ビットオーダーはプロセッサのアーキテクチャとは関係ありません.すなわち,コンパイラの実装によって,どちらから割り付けられるかが変わってきます.そのため,たとえ同じプロセッサをターゲットとしている場合であっても,コンパイラが変わればビットオーダーが変わる可能性があります.

コンパイラの実装の傾向としては,バイトオーダーがビッグエンディアンの場合は上位ビットから,リトルエンディアンの場合は下位ビットから割り付けられることが多いようですが,必ずしもそうとはかぎりませんし,コンパイルオプションによって切り替えられる場合もあるため,バイトオーダーとビットオーダーの対応は期待しないほうがよいでしょう.

4.6.2 境界調整

「境界調整」(アラインメント)は,オブジェクトが特定のバイトアドレスの倍数のアドレスに配置されることをいいます.これは,オブジェクトへのアクセスを効率良く行えるようにするための制約で,ほぼ完全にプロセッサのアーキテクチャに依存します.“ほぼ”と書いたのは,仮にプロセッサのアーキテクチャが境界調整を要求する場合であっても,分割してアクセスするなどの方法で,コンパイラがそれを吸収することもありうるからです.そして,実際にそのようなコンパイラも実在します.

境界調整は,1バイトのオブジェクトの場合は考慮する必要がありませんが,2バイト以上のオブジェクトの場合にはつねに配慮が必要になります.通常,16ビットCPUと呼ばれるプロセッサでは,16ビット以上のスカラ型は16ビット境界に配置されます.32ビットCPUと呼ばれるプロセッサでは,16ビットのスカラ型は16ビット境界に,32ビット以上のスカラ型は32ビット境界に配置されます.ただし,これは目安にすぎません.また,2の累乗ビット以外のスカラ型がどのような境界調整を要求するかはケースバイケースですので要注意です.具体的には,80ビットのlong double型の場合,16ビット境界に整列される場合もあれば,32ビット境界に整列される場合もあります(この場合,オブジェクトのサイズは96ビットになり,詰め物ビットが存在することになります).

クラスが要求する境界調整は,そのクラスを構成する要素のうち,最も大きな境界調整を要求するものに合わせるのが普通です.ただし,境界調整を決定する要素には,メンバーだけでなく,仮想関数テーブルへのポインタなど,ソース上には現れないものも含みます.

ある型が要求する境界調整のサイズを調べるには,次のようなマクロを用いるとよいでしょう.

【境界調整のサイズを調べるためのマクロ】

#define alignof(type)  offsetof(struct { char a; type b; }, b)

この方法は完全ではありませんが,ほとんどの場合は期待どおりに機能します.具体的にどんな場合にうまくいかないのかは思い付きませんが,規格上の保証はどうやらなさそうです.

なお,上記のマクロはCの場合には使えますが,C++ではコンパイルすることができません.なぜなら,C++では式中でクラスの定義を記述することができないからです.

そのため,まず,次のようなクラスを定義しておきます.

【上記マクロのためのクラス定義】

template <class T>
class alignof
{
    struct helper
    {
        char a;
        T b;
    };
public:
    static const std::size_t value = offsetof(helper, b);
};

そして,次のようにすれば,long型が要求する境界調整を知ることができます.

【境界調整のサイズを調べる】

std::size_t n = alignof<long>::value;

条件付きコンパイルを用いて,次のようにすれば,CでもC++でも同じシンタックスで型が要求する境界調整を調べることができます.

【条件付きコンパイルで境界調整を調べるためのプリプロセッサ命令】

#ifndef __cplusplus
#define alignof(type)   offsetof(struct { char a; type b; }, b)
#else
#define alignof(type)   (alignof<type>::value)
#endif

しかし,この方法にも弱点があります.それは,境界調整を調べることができるのは,C互換型(POD型)だけだということです.すなわち,スカラ型,C互換構造体型,C互換共用体型,それらの型の配列,およびそれらの型のcv修飾付き版だけであり,簡単にいえば,Cと同じように記述された型しか調べることができないということです.ただ,実際にはC互換型以外でも,多くの場合は調べることができるようです.期待どおりに動作しない可能性があることだけ心の片隅にとどめておけば,十分実用可能な方法といえるでしょう.

なお,C++の次期標準規格に導入される予定のクラスに,alignment_ofというものがあります.現行規格のコンパイラに追加する形の「TR1」(Technical Report 1)と呼ばれるライブラリにも含まれているので,比較的新しいバージョンのGCCなど,一部の処理系では利用できることもあるでしょう.

alignment_ofを使うには,次のようにします.

#include <cstddef>     // ← size_t
#include <type_traits> // ← alignment_of 
std::size_t n = std::tr1::alignment_of<long>::value;

COLUMN C互換型(POD型)

C互換型(POD型:Plain Old Data型)とは,スカラ型,C互換構造体型,C互換共用体型,それらの型の配列,およびそれらの型のcv修飾付き版のことです.C互換構造体/C互換共用体は,次のいずれも持たない集成体クラス/集成共用体のことです.

  • 非C互換構造体(またはその型の配列)である非静的データメンバー
  • 非C互換共用体(またはその型の配列)である非静的データメンバー
  • 参照型である非静的データメンバー
  • ユーザー定義のコピー代入演算子
  • ユーザー定義のデストラクタ

Cでは,ちょっと強引なコード,すなわち高級言語としてはふさわしくないような記述をすることも少なくないと思いますが,そうしたテクニックはC互換型以外の型に対しては通用しないことが多いので注意が必要です.たとえば,memset関数でオブジェクト全体をゼロクリアするような手法は,C互換型以外には使えません(未定義の動作になります)*7.memcpyでオブジェクトをコピーするのも同様です.組込み開発では,こうしたコードが現れる機会が多いでしょうから要注意です.

*7 厳密には,C互換型であっても,ポインタ型や浮動小数点型を含むオブジェクトをmemsetでゼロクリアするコードは移植性がありません.

4.6.3 int型のサイズ

C/C++の汎整数型は処理系によってサイズが異なります.標準規格では,それぞれの汎整数型が少なくともどれだけの表現範囲を持っているか,そして,それぞれの汎整数型の間の表現範囲の大小関係だけが決められています.

汎整数型の中でも,int型のサイズは16ビットと32ビットの処理系がそれなりに多く存在することもあり,入門書や解説書でも注意が促されることが多いようです.最近ではint型やlong型が64ビットの処理系もあるので,汎整数型のサイズを取り巻く状況はもう少し複雑になってきています.

そうしたなか,極力ソースコードの移植性を高めようということで,int型のようにサイズがよくわからない型は使用せず,int16型とか,int32型のような型を定義して,「それらを使うべし」とするコーディング規約もよく見かけます.それで本当に,int型のサイズに関する移植性の問題は解消されたのでしょうか?

残念ながら,世の中そう甘くはありません.どんな型定義を行おうとも,C言語にかかわっている以上,int型のサイズにまつわる問題は地の果てまで追いかけてきます.ここでは,そうしたint型のサイズに関する解説を行います.

汎整数拡張

「汎整数拡張」(Integral Promotion)という用語を見聞きされたことがあるでしょうか? この用語は,同じ意味にもかかわらず,いろいろ異なる表記をされることがあります.それも標準規格の中でです.

たとえば,C++規格であるJIS X3014:2003の中では,「汎整数昇格」という訳語が使われています.また,C99では「整数拡張」(Integer Promotion)という用語になぜか変わっています.すべて同じ意味ですが,ここでは「汎整数拡張」という表記に統一したいと思います.

汎整数拡張というのはいったいなんでしょうか? ごく簡単にいうと,何らかの演算を行うときには,オペランドの値がint型で表現できる場合はint型に,unsigned int型で表現できる場合はunsigned int型に,暗黙的に型変換が行われることです.「オペランドの値が……」と書きましたが,これはなにも,実行時に実際にどんな値なのかを調べるわけではなく,あくまでもオペランドの型だけを頼りに,静的に判断されます.

たとえば,次のような処理系を考えてみましょう.

表現範囲
char-128~+127
short-32768~+32767
int-2147483648~+2147483647
long-2147483648~+2147483647

近年では最も典型的な処理系ですが,この場合,汎整数拡張によって,char型とshort型はint型に型変換されることになります.また,unsigned char型とunsigned short型も,その表現範囲全体がint型でも表現できますから,int型に型変換されます.

つまり,元が符号無し整数型であっても,知らないうちに勝手にint型に変換される可能性があるわけです.

これは次のような状況で,勘違いを生み出す原因になります.

unsigned char c = 'A';
if (c - 0x20 < 0x5f)
{
     …
}

上記のコードは,ASCII文字が0x20~0x7eの範囲に収まっているかどうかを判定しようとしているようです.素直に書くと,次のとおりですが,比較を2回行う必要があるので,少しでも最適化しようとしたのでしょう.

if (0x20 <= c && c <= 0x7e)
{
     …
}

しかし,上記のコードは期待どおりには動いてくれません.というのも,上記のコードでは,次の箇所でcがunsigned char型なので,0x20を引いても負にはならず,0xe0になることを期待しているわけですが,実際には,減算の前に汎整数拡張*8が発生するので,int型として演算を行うことになります.

結果として,c - 0x20は-0x10になるので,期待は完全に裏切られます.

c - 0x20

他の例を挙げてみましょう.

char a = 0;
if (sizeof(+a) == sizeof(a))
{
     …
}

ちょっとわざとらしい例ですが,上記のコードでは,ifの条件式は真になるでしょうか? それとも偽になるでしょうか?

結果は偽になります.単項の+演算子というのは,何も行わない演算子だと理解されていることが多いと思います.しかし,この演算子のオペランドも汎整数拡張が行われます.それに対して,sizeof演算子のオペランドは汎整数拡張が行われません.結果として,左辺の+aはint型であり,右辺のaはchar型になるので,今回仮定している処理系では両辺は等しくなりません.

このように,たとえどんなに型定義などを使ってint型のサイズを隠蔽したとしても,何らかの演算を行うと,int型のサイズの影響が露骨に現れることになります.

整数定数

整数定数(いわゆるリテラル)にもint型のサイズが関係してきます.整数定数の型は,(ちょっと複雑ですが)次の手順で決定されます.

1 int型の表現範囲であればint型 2 int型の表現範囲になく,unsigned int型の表現範囲にある8進または16進数の場合はunsigned int型 3 long型の表現範囲であればlong型 4 そうでなければ,unsigned long型

ただし,C99の場合には,long long型が存在するので,long型の表現範囲にない10進定数はlong long型になります(以下省略).

このように,整数定数の型は,その値によって決まります.すなわち,int型のサイズによって,整数定数がどんな型になるかが変わるわけです.特に,0xffffのような整数定数は,unsigned int型になったりint型になったりするので要注意です.

ここで,整数定数の型に関するありがちな勘違いの具体例を挙げてみます.なお,今度は,先ほどとは違ってint型が16ビットで,その表現範囲が-32758~+32767の処理系について考えてみることにします.

次のコードのおかしな点に気づくでしょうか?

typedef int count_t;
#define COUNT_MAX  32767
#define COUNT_MIN  (-32768)

おかしな点はCOUNT_MINマクロの定義にあります.int型の表現範囲は-32768~+32767なので,int型に定義されているcount_tの最小値を表すCOUNT_MINマクロが(-32768)なのは当然のような気がします.

しかし,値はともかく,問題はCOUNT_MINの型にあります.32768というのは,int型(表現範囲は-32768~+32767と仮定)の表現範囲に収まりません.また,8進定数でも16進定数でもありませんから,32768の型はlong型です.long型のオペランドに単項の-演算子を付けても,やはりlong型です.

つまり,上記のコードのようなCOUNT_MINマクロの定義では,本来int型の定数式に展開されるべきであるにもかかわらず,long型になってしまうわけです.では,どのように定義すればint型になるのでしょうか?

それは,次のようにします.

#define COUNT_MIN  (-32767-1)

今度は,-32767から1を引いています.32767はint型の表現範囲に収まっていますからint型です.そして,それに単項の-演算子を付けてもやはりint型です.また,int型である1との減算を行っても,結果はやはりint型になります.

いかがでしょうか? int型のサイズを隠蔽するために別の型を定義するコーディング規約は多いのですが,それをやってしまうと,一見しただけでは式や定数の振る舞いがわからなくなってしまいます.なぜなら,定義された型の本当の型がわからないからです.

*8 より厳密には,通常の算術型変換の過程として汎整数拡張が起きます.

4.6.4 ヘッダ名

ヘッダ名の指定方法が大きく分けて2つあることは,C/C++を使われている方であればよくご存じのはずです.すなわち,<…>の形式と"…"の形式です.まずは,この2つの形式の違いから見ていきたいと思います.

まず,<…>の形式ですが,これは本来は処理系が提供するヘッダを指定するためのものですが,規格上はやや曖昧で,「処理系で処理可能なヘッダ」という,わかったようなわからないような記述になっています.そして,このヘッダというのは必ずしもファイルである必要はありません.また,どのようにしてヘッダの探索場所を指定するか,どのようにしてヘッダを識別するかも処理系定義になっています.

次に,"…"形式ですが,こちらは基本的にはソースファイルを取り込むためのものです.そして,指定したソースファイルの探索方法は処理系定義です.

最も一般的な探索手順は,#include指令が記述されたソースファイルと同じディレクトリをまずは探索し,そこで見つからなければ,そのソースファイルが取り込まれたソースファイルと同じディレクトリを探索し……というように,最初に#include指令を使ったソースファイルと同じディレクトリまで遡るというものです.ほかには,つねに作業ディレクトリを探索するという処理系もあります.

また,標準規格には"…"形式に関する箇所(「JIS X3010:2003 6.10.2 ソースファイル取込み」)で,「この探索を提供していない場合」という記述があるので,"…"形式を指定しても<…>と同じになってしまう場合があるようです.この場合に,ヘッダがファイルではないとすると,ヘッダを追加する術はまったくなくなってしまうわけですが,インタープリタのようなものを除けば,まずこのような処理系に遭遇することはないと思います.

ところで,これを書くとWindows環境で作業されている方は驚かれるかもしれませんが,ヘッダ名の中に逆斜線(\)(逆斜線の代わりに\記号を使っているのであれば\記号)が含まれていた場合の動作は未定義になります.ほかにも',",//,/*が含まれていた場合も動作が未定義になります.また,規格上識別されることが保証されるヘッダ名は,英字で始まる英数字の並びの後に,1つのピリオドと英字1文字が続く名前ということになっています.なお,C++では,1個以上の非数字の並びの後に,1つのピリオドと非数字1文字が続く名前ということになっています.また,英字の大文字・小文字の区別があるとはかぎらず,ピリオドの前の英数字の並びのうち確実に有効なのは6文字(C99では8文字)です.C++では字数の制限はないようです.

このように見ていくと,規格厳密合致プログラムを書こうとすると,ヘッダファイルを自作することは不可能だということになります.しかし,この件に関しては,規格厳密合致プログラムにこだわることをやめ,現実に十中八九問題のない選択を行うということであれば,かなり状況が緩和されます.

まず,ヘッダ名に指定できる名前の有意文字数の制限は実質的になくなります.ただし,Windowsのことを考えると大文字・小文字に依存するのは止めたほうがよいでしょう.また,ディレクトリの区切りには,\ではなく/を使うべきです.

次に,ヘッダファイルの探索順序についてですが,"…"形式の探索順序を指定できるコンパイラは稀ですが,<…>形式であれば,「-I」オプションやそれに相当するものがほとんどのコンパイラで使えるので,これに依存することは問題ないでしょう.というわけで,"…"の探索順序にさえ依存しなければ,ほぼ移植性のあるコードを書くことができるはずです.